/**
 * create by jiangzhiwei
 * 2022-02-21 v1.0.1 显示/隐藏折线后，纵坐标轴随之变化
 * 2022-02-22 v1.0.2 美化坐标轴刻度值
 * 2022-02-23 v1.0.3 新增仅显示一条折线模式
 * 2022-02-24 v1.0.4 绘制出离散的点
 * 2022-04-01 v1.0.5 加上浏览器不提供actualBoundingBoxAscent等属性而导致图表不显示的错误信息
 * 2022-04-02 v1.0.6 修改格式化刻度数字时出现小数时逗号显示异常的问题
 * 2022-06-15 v1.1.0 加上是否连接离散的点配置项，默认连接
 * 2023-01-11 v1.2.0 增加【不显示当前时间时，第一个数据对应左侧坐标还是右侧坐标】的配置
 */
class Graph {
  /**
   * 构造器
   * @param  {Object} dom canvas节点
   * @param  {Object} option 配置信息{ title, line, axis, leftAxis, rightAxis, ... }
   * @author Jiangzhiwei
   * @time   2021-11-03T22:22:00+0800
   */
  constructor (dom, option) {
    this.init(dom, option)
    this.resize()
    this.run()
  }

  init (dom, option) {
    this.option = option
    this.canvas = dom
    this.ctx = this.canvas.getContext('2d')
    this.cacheCanvas = document.createElement('canvas')
    this.cacheCtx = this.cacheCanvas.getContext('2d')
    this.isActive = true // 标记有效，用于是否继续循环绘制
    this.isMoving = true // 坐标轴是否正在移动
    this.ratio = 2 // 缩放比例
    this.regex = /(\d{4})\D(\d{2})\D(\d{2})\D(\d{2})\D(\d{2})\D(\d{2})/ // 解析年月日时分秒的正则
    this.surplusRecords = [] // 记录各折线不再使用的数据在数组中的最大序号
    this.clearSurplusNum = option.clearSurplusNum || 50 // 无效折线数据超过多少时，进行移除
    this.maxUpdateTime = option.maxUpdateTime || 1 // 数据最长更新时间，主要用于判断数据是否连续
    // 打印渲染时间相关
    this.totalRenderTime = 0 // 总渲染时间
    this.renderTimeCount = 0 // 渲染次数
    this.renderTimes = 100 // 渲染多少次取平均值
    this.printRenderCostTime = option.printRenderCostTime // 是否打印每次渲染耗时，用于分析性能，默认不打印

    // 间距
    this.padding = option.padding || 20 // 与四周的内间距

    // 字体
    this.fontFamily = option.fontFamily || 'Arial' // 所有字体

    // 标题
    const title = option.title || {}
    this.title = {
      text: title.text || '', // 标题文本
      color: title.color || '#333333', // 标题颜色
      align: title.align || 'left', // 标题水平对齐，left right center
      fontSize: title.fontSize || 20 // 标题字体大小
    }

    // 折线
    const defaultColors = ['#fe5722', '#00BFFF', '#01DF01', '#FFFF00', '#FE9A2E', '#BF00FF']
    const line = option.line || {}
    this.line = {
      // 线条相关
      data: line.data || [], // 折线数据
      axis: line.axis || [], // 所属坐标轴，如left、right，如果没有对应坐标轴，则作为额外数据
      colors: Object.assign(defaultColors, line.colors), // 折线依次的颜色
      /*
        对应数据中的key数组，如[{ k1, k2 }, ...]的数据，则keys为['k1', 'k2', 'k3']，最后一项k3对应时间
       */
      keys: line.keys || [],
      extraUnits: line.extraUnits || [], // 额外数据的单位
      showDataPoint: line.showDataPoint, // true时显示数据点，默认不显示。
      dataPointRadius: line.dataPointRadius || 1.5, // 数据点半径大小。5以下时，绘制耗时变化明显
      isSmooth: line.isSmooth, // 线条是否平滑，默认不平滑
      connectDispersedPoint: line.connectDispersedPoint !== false, // 是否连接离散的点。默认连接

      // 名称相关
      names: line.names || [], // 折线名称
      nameMargin: line.nameMargin, // 多个折线名称之间的间距
      showSingleName: line.showSingleName, // 只有一条折线时是否显示折线名，默认不显示
      align: line.align || 'center', // 折线名称水平对齐，left、right、center
      color: line.color || '#333333', // 折线名称颜色
      fontSize: line.fontSize || 18, // 折线名称字体大小
      showBorder: line.showBorder, // 是否显示图形边框
      borderColor: line.borderColor || '#cccccc', // 所有圆圈边框颜色
      letterSpacingVer: line.letterSpacingVer || 5, // 折线名称换行时，上下行之间的距离
      marginTop: line.marginTop || 5, // 折线名称与上方标题的间距
      marginBottom: line.marginBottom || 5, // 折线名称与下方坐标轴的间距

      // 隐藏相关
      showSingle: line.showSingle, // 是否同时只显示一条折线数据
      hideIndexes: line.hideIndexes || [], // 需要隐藏的折线的序号
      hideColor: line.hideColor || '#A4A4A4' // 隐藏折线后，对应折线圆圈的颜色
    }
    // 如果只显示一条折线，则从第二条折线开始隐藏
    if (this.line.showSingle) {
      this.line.hideIndexes = this.line.names.map((item, index) => index).filter(item => item !== 0)
    }

    // 坐标轴
    const axis = option.axis || {}
    this.axis = {
      len: axis.len || 5, // 刻度线长度
      padding: axis.padding || 5, // 刻度线与刻度值的间距
      marginLeft: axis.marginLeft || 50, // 纵坐标轴与左边界的距离的一部分
      marginRight: axis.marginRight || 50, // 纵坐标轴与右边界的距离的一部分
      marginBottom: axis.marginBottom || 25, // 横坐标轴与底边的距离
      color: axis.color || '#333333', // 坐标轴字体颜色
      lineColor: axis.lineColor || '#333333', // 坐标轴线条颜色
      fontSize: axis.fontSize || 16, // 坐标轴字体大小
      showHorLine: axis.showHorLine, // 是否显示纵坐标刻度水平辅助线，默认不显示
      horLineColor: axis.horLineColor || '#999999' // 纵坐标刻度水平辅助线颜色
    }

    // 左侧坐标轴
    const leftAxis = option.leftAxis || {}
    this.leftAxis = {
      unit: leftAxis.unit || '', // 坐标轴单位
      num: leftAxis.num || 5, // 刻度格数
      min: leftAxis.min, // 最小值
      max: leftAxis.max // 最大值
    }

    // 右侧坐标轴
    const rightAxis = option.rightAxis || {}
    this.rightAxis = {
      unit: rightAxis.unit || '', // 坐标轴单位
      num: rightAxis.num || 5, // 刻度格数
      min: rightAxis.min, // 最小值
      max: rightAxis.max // 最大值
    }

    // 底部坐标轴
    const bottomAxis = option.bottomAxis || {}
    this.bottomAxis = { // 横坐标
      showNowTime: bottomAxis.showNowTime, // 是否是显示当前时间。false则根据数据对应时间，最小时间对应最小值
      dataAlign: bottomAxis.dataAlign || 'left', // 不显示当前时间时，第一个数据对应左侧坐标还是右侧坐标。受delay影响。
      delay: typeof bottomAxis.delay === 'undefined' ? 5 : bottomAxis.delay, // 最大显示到几秒前的数据，配合showNowTime === true时使用
      num: bottomAxis.num || 20, // 刻度格数
      size: bottomAxis.size || 5, // 每格秒数
      showTimeGap: bottomAxis.showTimeGap || 15 // 每间隔多少显示刻度值
    }

    // 辅助线
    const assist = option.assist || {}
    this.assist = {
      color: assist.color || '#333333', // 辅助线颜色
      dash: assist.dash || [], // 虚线数组，如[5, 15]
      circleRadius: assist.circleRadius || 5, // 辅助线对齐折线的提示点半径大小
      showPointBorder: assist.showPointBorder, // 提示点显示边框
      borderColor: assist.borderColor || this.line.borderColor // 辅助线提示圆圈边框颜色
    }

    // 数据面板
    const pannel = option.pannel || {}
    this.pannel = { // 面板属性
      noDataText: pannel.noDataText || '无数据', // 没有数据时显示文字
      separator: pannel.separator || '：', // 折线名与数据之前的分隔符
      showUnit: pannel.showUnit, // 是否显示单位，默认显示
      fontSize: pannel.fontSize || 16, // 字体大小
      color: pannel.color || '#ffffff', // 字体颜色
      bgColor: pannel.bgColor || 'rgba(0, 0, 0, 0.7)', // 背景颜色
      borderRadius: pannel.borderRadius || 5, // 圆角大小
      margin: pannel.margin || 20, // 面板与辅助线的间距
      padding: pannel.padding || 10, // 面板间距
      letterSpacingVer: pannel.letterSpacingVer || 8, // 行间距
      figureMargin: pannel.figureMargin || 8, // 圆圈与文字之间的间距
      figureBorderColor: pannel.figureBorderColor || this.line.borderColor // 圆圈边框颜色
    }

    this.watchInterval = option.watchInterval || 100 // 定时监听的时间间隔

    this.watchResize() // 定时监听大小是否发生变化
    // this.resizeEvent = () => {
    //   this.resize()
    // }
    // window.addEventListener('resize', this.resizeEvent)
  }

  /**
   * 绘制文本
   * @param  {Number} options.x 对齐点x坐标
   * @param  {Number} options.y 对齐点y坐标
   * @param  {String} options.text 文本
   * @param  {String} options.color 颜色
   * @param  {Number} options.fontSize 字体大小
   * @param  {String} options.align 水平对齐，left、right、center
   * @param  {String} options.verticalAlign 垂直对齐，top、bottom、middle
   * @author Jiangzhiwei
   * @time   2021-11-07T16:51:48+0800
   */
  drawText ({
    x, y, text, color = '#333333', fontSize = 30, align = 'left', verticalAlign = 'bottom'
  }) {
    this.cacheCtx.save()
    this.cacheCtx.font = `${fontSize}px ${this.fontFamily}`
    this.cacheCtx.fillStyle = color
    if (align === 'right') {
      x -= this.cacheCtx.measureText(text).width
    } else if (align === 'center') {
      x -= this.cacheCtx.measureText(text).width / 2
    }
    this.cacheCtx.textBaseline = verticalAlign
    this.cacheCtx.fillText(text, x, y)
    this.cacheCtx.restore()
  }

  /**
   * 绘制大标题与折线名称
   * @author Jiangzhiwei
   * @time   2021-11-05T00:17:13+0800
   */
  drawTitles () {
    this.drawTitle()
    this.measureLineNames()
    this.drawLineNames()
  }

  /**
   * 绘制大标题
   * @author Jiangzhiwei
   * @time   2021-11-05T00:17:33+0800
   */
  drawTitle () {
    const title = this.title
    let align = 'left'
    let startX = this.padding * this.ratio
    if (title.align === 'right') { // 仅右对齐时，align为right
      align = 'right'
      startX = this.canvas.width - this.padding * this.ratio
    }
    if (title.align === 'center') {
      this.cacheCtx.font = `${this.title.fontSize * 2}px ${this.fontFamily}`
      const mt = this.getTextInfo(title.text)
      startX = (this.canvas.width - mt.width) / 2
    }
    this.drawText({
      x: startX,
      y: (this.title.fontSize + this.padding) * this.ratio,
      text: title.text,
      color: title.color,
      fontSize: title.fontSize * this.ratio,
      align
    })
  }

  /**
   * 计算所有折线名字总长度，为了在后面使其对齐
   * 计算所有折线名字所占矩形区域的数据，用于点击显示/隐藏折线
   * 计算坐标轴端点位置
   * @author Jiangzhiwei
   * @time   2021-11-04T23:49:22+0800
   */
  measureLineNames () {
    this.cacheCtx.font = `${this.line.fontSize * this.ratio}px ${this.fontFamily}`
    const mt = this.getTextInfo('国') // 参考一个标准汉字高度
    this.lineNameY = (this.padding + this.title.fontSize + this.line.fontSize / 2 +
      this.line.marginTop) * this.ratio
    this.lineNameCircleOffsetY = mt.actualBoundingBoxDescent // 调整高度，使之与汉字中心对齐
    this.lineNamePadding = mt.width / 4 // 圆圈与汉字之间的间距
    this.lineNameMargin = this.line.nameMargin * this.ratio || mt.width / 2 // 标题之间的间距
    this.lineNameCircleRadius = mt.height / 2 // 标题中的圆圈半径
    // 计算总长度
    const nameInfos = []
    let maxWidth = 0 // 最长宽度
    let currentWidth = 0 // 当前行宽度
    let y = 0 // y偏移位置。当前行顶部距离起始行顶部的距离，如果有换行，则不为0
    this.line.names.forEach((name, index) => {
      if (index >= this.line.axis.length) { // 没有对应坐标轴，因此不加入宽度计算
        return
      }
      const width = this.lineNameCircleRadius * this.ratio +
        this.lineNamePadding + this.getTextInfo(name).width // 圆宽+间距+字宽
      if (index !== 0) { // 不是第一个。也就是说，如果第一个名字就超长，目前不作处理
        if (currentWidth + width + this.lineNameMargin > this.canvas.width) { // 超过总长度，则换行
          y += mt.height + this.line.letterSpacingVer * this.ratio
          currentWidth = 0
        } else { // 未超过总长度
          currentWidth += this.lineNameMargin // 加上间距
        }
      }
      nameInfos.push({ x: currentWidth, y, width })
      currentWidth += width
      if (maxWidth < currentWidth) { // 比较最长宽度
        maxWidth = currentWidth
      }
    })
    if (this.line.align === 'right') { // 右对齐
      this.lineNameStartX = this.canvas.width - maxWidth - this.padding * this.ratio
    } else if (this.line.align === 'center') { // 居中对齐
      this.lineNameStartX = (this.canvas.width - maxWidth) / 2
    } else { // 左对齐
      this.lineNameStartX = this.padding * this.ratio
    }
    // 计算所有折线名字所占矩形区域的数据
    this.lineNameRectInfos = []
    const startX = this.lineNameStartX // 左上角x
    const startY = this.lineNameY + this.lineNameCircleOffsetY - mt.height / 2 // 左上角y
    this.line.names.forEach((name, index) => {
      if (index >= this.line.axis.length) { // 隐藏数据不显示名字，因此不记录区域信息
        return
      }
      this.lineNameRectInfos.push({
        x: startX + nameInfos[index].x,
        y: startY + nameInfos[index].y,
        width: nameInfos[index].width,
        height: mt.height,
        name
      })
    })
    // 计算坐标轴端点位置
    this.top = startY + y + mt.height +
      (this.line.marginBottom + this.axis.fontSize * 1.5) * this.ratio
    this.left = (this.padding + this.axis.marginLeft) * this.ratio // 左侧纵坐标轴距离canvas左边界的距离
    this.right = this.canvas.width -
      (this.axis.marginRight + this.padding) * this.ratio // 右侧纵坐标轴距离canvas右边界的距离
    this.bottom = this.canvas.height -
      (this.axis.marginBottom + this.padding) * this.ratio // 横坐标轴距离canvas下边界的距离
  }

  /**
   * 绘制折线名称
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T18:12:06+0800
   */
  drawLineNames () {
    if (this.lineNameRectInfos.length > 1 || this.line.showSingleName) {
      this.lineNameRectInfos.forEach((item, index) => {
        let x = item.x
        const y = item.y + item.height / 2
        const color = this.line.hideIndexes.some(hideIndex => hideIndex === index)
          ? this.line.hideColor : this.line.colors[index]
        // 绘制圆圈
        x += this.drawCircle({
          x,
          y: y + this.lineNameCircleOffsetY,
          r: this.lineNameCircleRadius,
          color,
          borderColor: this.line.showBorder === false ? null : this.line.borderColor,
          align: 'left'
        }) + this.lineNamePadding
        this.drawText({
          x,
          y,
          text: item.name,
          color: this.line.color,
          fontSize: this.line.fontSize * this.ratio,
          verticalAlign: 'middle'
        }) // 绘制折线名称
        x += this.cacheCtx.measureText(item.name).width + this.lineNameMargin
      })
    }
  }

  /**
   * 绘制左方坐标轴
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T18:11:00+0800
   */
  drawLeftAxis () {
    this.drawVerticalAxis('left')
  }

  /**
   * 绘制右方坐标轴
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T18:11:47+0800
   */
  drawRightAxis () {
    this.drawVerticalAxis('right')
  }

  /**
   * 绘制纵坐标轴公共方法
   * @param  {String} axis left 或 right
   * @author Jiangzhiwei
   * @time   2021-11-06T19:40:55+0800
   */
  drawVerticalAxis (axis) {
    const fontSize = this.axis.fontSize * this.ratio
    this.cacheCtx.beginPath()
    this.cacheCtx.moveTo(this[axis], this.top)
    this.cacheCtx.lineTo(this[axis], this.bottom)
    this.cacheCtx.strokeStyle = this.axis.lineColor
    this.cacheCtx.lineWidth = 2
    this.cacheCtx.stroke()
    // 绘制单位
    this.drawText({
      x: this[axis],
      y: this.top - this.axis.fontSize * this.ratio,
      text: this[`${axis}Axis`].unit,
      color: this.axis.color,
      fontSize,
      align: 'center',
      verticalAlign: 'middle'
    })
    // 绘制刻度
    const num = this[`${axis}Axis`].num
    const step = (this.bottom - this.top) / num
    let lineOffsetX
    let textOffsetX
    let align
    if (axis === 'left') {
      lineOffsetX = -this.axis.len * this.ratio
      textOffsetX = -(this.axis.len + this.axis.padding) * this.ratio
      align = 'right'
    } else {
      lineOffsetX = this.axis.len * this.ratio
      textOffsetX = (this.axis.len + this.axis.padding) * this.ratio
      align = 'left'
    }
    // console.time()
    const finishX = this[axis] + lineOffsetX
    const x = this[axis] + textOffsetX
    const values = this[`${axis}AxisValues`]
    const yArr = [] // 辅助线数组
    this.cacheCtx.beginPath()
    for (let i = 0; i <= num; i++) {
      const y = Math.floor(this.bottom - i * step)
      if (i !== 0) { // 第一条辅助线与横坐标重合，不会绘制，因此不加入
        yArr.push(y)
      }
      this.cacheCtx.moveTo(this[axis], y)
      this.cacheCtx.lineTo(finishX, y)
      this.drawText({
        x,
        y,
        text: this.formatValue(values[i]),
        color: this.axis.color,
        fontSize,
        align,
        verticalAlign: 'middle'
      })
    }
    this.cacheCtx.stroke()
    if (this.axis.showHorLine) { // 如果需要显示纵坐标水平辅助线
      this.cacheCtx.beginPath()
      this.cacheCtx.save()
      this.cacheCtx.strokeStyle = this.axis.horLineColor
      yArr.forEach(y => {
        this.cacheCtx.moveTo(this.left, y)
        this.cacheCtx.lineTo(this.right, y)
      })
      this.cacheCtx.stroke()
      this.cacheCtx.restore()
    }
    // console.timeEnd()
  }

  /**
   * 绘制下方坐标轴
   * @author Jiangzhiwei
   * @time   2021-11-04T23:50:07+0800
   */
  drawBottomAxis () {
    this.cacheCtx.beginPath()
    this.cacheCtx.moveTo(this.left, this.bottom)
    this.cacheCtx.lineTo(this.right, this.bottom)
    this.cacheCtx.strokeStyle = this.axis.lineColor
    this.cacheCtx.lineWidth = 2
    this.cacheCtx.stroke()

    // 绘制刻度
    const num = this.bottomAxis.num
    const gap = (this.right - this.left) / num
    let offsetX = 0
    let offsetValue = 0
    const remain = this.bottomAxisMin % this.bottomAxis.size
    if (remain !== 0) {
      offsetX = (this.bottomAxis.size - remain) * this.bottomAxisStep
      offsetValue = this.bottomAxis.size - remain
    }
    // this.bottomAxisStep = gap / this.bottomAxis.size
    // console.log(this.bottomAxisStep, this.right - this.left)
    const times = offsetX === 0 ? num : num - 1
    for (let i = 0; i <= times; i++) {
      const x = Math.floor(this.left + i * gap + offsetX)
      const xValue = this.bottomAxisMin + offsetValue + i * this.bottomAxis.size
      let len = this.axis.len * this.ratio // 刻度线长度
      if (xValue % this.bottomAxis.showTimeGap === 0) { // 是间隔的显示刻度值时间
        let text = xValue % 60
        if (text === 0) { // 如果是整分钟，则显示完整时间
          text = this.formatTime(new Date(xValue * 1000))
          len *= 1.5
        }
        this.drawText({
          x,
          y: this.bottom + (this.axis.len + this.axis.padding) * this.ratio,
          text,
          color: this.axis.color,
          fontSize: this.axis.fontSize * this.ratio,
          align: 'center',
          verticalAlign: 'top'
        })
      }
      this.cacheCtx.beginPath()
      this.cacheCtx.moveTo(x, this.bottom)
      this.cacheCtx.lineTo(x, this.bottom + len)
      this.cacheCtx.stroke()
    }
  }

  /**
   * 画所有折线
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T11:02:14+0800
   */
  drawLines () {
    this.cacheCtx.save()
    // 裁剪
    this.cacheCtx.rect(this.left + 1, this.top + 1, this.right - this.left - 2,
      this.bottom - this.top - 2)
    this.cacheCtx.clip()
    // 画线
    this.cacheCtx.lineWidth = 3
    this.line.data.forEach((item, index) => {
      if (this.line.hideIndexes.every(item => item !== index)) {
        this.drawLine(item, index)
      }
    })
    // 画辅助线
    this.drawAssistLine()
    this.cacheCtx.restore()
  }

  /**
   * 画一条折线
   * @param    {Array} data 折线数据
   * @param    {String} color 折线颜色
   * @param    {String} axis left or right
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T11:02:47+0800
   */
  drawLine (data, index) {
    const color = this.line.colors[index]
    const axis = this.line.axis[index]
    const radius = this.line.dataPointRadius * this.ratio
    this.cacheCtx.strokeStyle = color
    // 过滤目前与画折线无关的数据
    let idx = -1
    const activeData = data.filter((item, i) => {
      // const tx = this.getTimeSeconds(item.x)
      if (item.x < this.bottomAxisMin - 1) {
        idx = i
        return false
      }
      return item.x <= this.bottomAxisMax + 1
    })
    this.surplusRecords[index] = idx // 记录下不需要的数据的序号
    let isLineHead = true // 是否是一条线的开始，主要用于标志这个点的前方已断开
    this.cacheCtx.beginPath()
    activeData.forEach((item, idx) => {
      const x = this.getPosX(item.x)
      const y = this.getPosY(item.y, axis)
      if (index < this.line.axis.length) { // 有对应坐标轴
        if (idx === 0) { // 如果是第一个点，则移动过去
          this.cacheCtx.moveTo(x, y)
          item.k = 0
          isLineHead = true
        } else { // 如果不是第一个点，则判断时间是否连续
          const prevItem = activeData[idx - 1]
          if (item.x - prevItem.x <= this.maxUpdateTime || this.line.connectDispersedPoint) { // 未超过更新时间 或 连接离散点，表示线条连续
            if (this.line.isSmooth) { // 线条是否平滑
              const nextItem = activeData[idx + 1]
              if (nextItem && nextItem.x - item.x <= this.maxUpdateTime) { // 有下一个连续点
                if ((nextItem.y - item.y) * (item.y - prevItem.y) > 0) { // 表示同时上升或下降
                  // y = k * x + b =>
                  // y = k * x + b; y2 = k * x2 + b =>
                  // y2 - y = (x2 - x) * k =>
                  // k = (y2 - y) / (x2 - x); b = y - k * x
                  // 以当前点与下一个点的连线计算斜率，效果没有下面的好
                  // const x2 = this.getPosX(nextItem.x)
                  // const y2 = this.getPosY(nextItem.y, axis)
                  // item.k = (y2 - y) / (x2 - x)
                  // item.b = y - item.k * x
                  // 以前一个点与当前点的连线计算斜率
                  const x0 = this.getPosX(prevItem.x)
                  const y0 = this.getPosY(prevItem.y, axis)
                  item.k = (y - y0) / (x - x0)
                  item.b = y - item.k * x
                  const cp1 = this.calcControlPos(prevItem.k, item.k, prevItem.b, x0, y0, x, y)
                  const cp2 = this.calcControlPos(prevItem.k, item.k, item.b, x0, y0, x, y, true)
                  this.cacheCtx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x, y)
                  // this.drawText({ x, y: y - 10, text: Math.floor(item.k * 100) / 100, align: 'center' })
                } else { // 一个上升，一个下降
                  this.drawNotFollowLinePath(item, prevItem, axis, x, y)
                  // item.k = 0
                  // const x0 = this.getPosX(prevItem.x)
                  // const y0 = this.getPosY(prevItem.y, axis)
                  // const cp1 = this.calcControlPos(prevItem.k, item.k, prevItem.b, x0, y0, x, y)
                  // const cp2 = this.calcControlPos(prevItem.k, item.k, item.b, x0, y0, x, y, true)
                  // this.cacheCtx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x, y)
                }
              } else { // 没有下一个连续点
                this.drawNotFollowLinePath(item, prevItem, axis, x, y)
              }
            } else { // 线条不平滑
              this.cacheCtx.lineTo(x, y)
            }
            isLineHead = false
          } else { // 不连续，则重新开始画线
            if (isLineHead) { // 如果上个点是一条线的开头，则此时表明上个点是离散的
              // 绘制离散点
              const px = this.getPosX(prevItem.x)
              const py = this.getPosY(prevItem.y, axis)
              this.cacheCtx.arc(px, py, 1, 0, Math.PI * 2)
            }
            this.cacheCtx.moveTo(x, y)
            item.k = 0
            isLineHead = true
          }
        }
        if (this.line.showDataPoint) { // 如果要显示数据点，则绘制
          if (radius <= 10) { // 半径10以下画线更快，循环次数变化后，耗时变化明显
            for (let i = radius; i > 2; i--) {
              this.cacheCtx.arc(x, y, i, 0, Math.PI * 2)
            }
          } else { // 半径10以上画圈更快，半径变化后，耗时变化不明显
            this.cacheCtx.stroke()
            this.drawCircle({ x, y, r: radius, color, borderColor: color })
            this.cacheCtx.beginPath()
          }
          this.cacheCtx.moveTo(x, y)
        }
      }
      // 记录下数据点的值，用于在信息面板中显示
      if (this.areaData[item.x] && typeof this.areaData[item.x].values[index] === 'undefined') {
        this.areaData[item.x].values[index] = item.y
      }
    })
    this.cacheCtx.stroke()
  }

  /**
   * 计算控制点位置
   * @param  {Number} k1 前一个点的斜率
   * @param  {Number} k2 当前点的斜率
   * @param  {Number} b 要计算的对应控制点的截距，前一个点的对应第一个，当前点的对应第二个
   * @param  {Number} x1 前一个点的x
   * @param  {Number} y1 前一个点的y
   * @param  {Number} x2 当前点的x
   * @param  {Number} y2 当前点的y
   * @param  {Boolean} isCurrent 是否是计算第二个控制点，对应当前点
   * @return {Object} 控制点位置
   * @author Jiangzhiwei
   * @time   2021-11-15T00:36:10+0800
   */
  calcControlPos (k1, k2, b, x1, y1, x2, y2, isCurrent) {
    let x
    let y
    if (isCurrent) { // 当前点
      if (k1 === 0) {
        k1 = (y2 - y1) / (x2 - x1)
      }
      if (k2 === 0) {
        x = (x1 + x2) / 2
        y = y2
      } else if (Math.abs(k1) > Math.abs(k2)) { // 由陡至缓，控制点2以x轴来约束
        x = (x1 + x2) / 2
        // y = kx + b
        y = k2 * x + b
      } else { // 由缓至陡，控制点2以y轴来约束
        y = (y1 + y2) / 2
        // y = k * x + b =>
        // x = (y - b) / k
        x = (y - b) / k2
      }
    } else { // 上一个点
      if (k2 === 0) {
        k2 = (y2 - y1) / (x2 - x1)
      }
      if (k1 === 0) {
        x = (x1 + x2) / 2
        y = y1
      } else if (Math.abs(k1) > Math.abs(k2)) { // 由陡至缓，控制点1以y轴来约束
        y = (y1 + y2) / 2
        // y = k * x + b =>
        // x = (y - b) / k
        x = (y - b) / k1
      } else { // 由缓至陡，控制点1以x轴来约束
        x = (x1 + x2) / 2
        // y = kx + b
        y = k1 * x + b
      }
    }
    return { x, y }
  }

  /**
   * 公共方法
   * @param  {Object} item 当前点信息
   * @param  {Object} prevItem 前一个点信息
   * @param  {String} axis left轴 or right轴
   * @param  {Number} x 当前点的x
   * @param  {Number} y 当前点的y
   * @author Jiangzhiwei
   * @time   2021-11-15T00:45:21+0800
   */
  drawNotFollowLinePath (item, prevItem, axis, x, y) {
    item.k = 0
    const x0 = this.getPosX(prevItem.x)
    const y0 = this.getPosY(prevItem.y, axis)
    const cp1 = this.calcControlPos(prevItem.k, item.k, prevItem.b, x0, y0, x, y)
    const cp2 = this.calcControlPos(prevItem.k, item.k, item.b, x0, y0, x, y, true)
    this.cacheCtx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x, y)
  }

  /**
   * 画实心圆
   * @param  {Number} options.x 坐标
   * @param  {Number} options.y 坐标
   * @param  {Number} options.r 半径
   * @param  {String} options.color 填充颜色
   * @param  {String} options.borderColor 边框颜色
   * @param  {String} options.align 对齐
   * @return {Number} 圆宽
   * @author Jiangzhiwei
   * @time   2021-11-04T23:50:29+0800
   */
  drawCircle ({ x, y, r = 15, color, borderColor, align }) {
    if (align === 'left') {
      x += r
    }
    this.cacheCtx.save()
    this.cacheCtx.beginPath()
    this.cacheCtx.arc(x, y, r, 0, Math.PI * 2)
    this.cacheCtx.fillStyle = color
    this.cacheCtx.fill()
    if (borderColor) { // 需要边框时绘制边框
      this.cacheCtx.strokeStyle = borderColor
      this.cacheCtx.stroke()
    }
    this.cacheCtx.restore()
    return r * 2
  }

  /**
   * 在坐标轴范围内移动，显示辅助线与信息面板
   * @param  {Object} event 点移动事件
   * @author Jiangzhiwei
   * @time   2021-11-06T00:10:59+0800
   */
  mousemove (event) {
    const index = this.getPointInNameAreaIndex(event)
    if (index === -1) { // 不在折线名称区域
      this.canvas.style.cursor = 'default'
      if (this.isPointInArea(event)) { // 在坐标轴区域中
        const point = this.getPointInCanvas(event)
        this.assistLineX = point.x
      } else { // 不在坐标轴区域中
        this.assistLineX = null
      }
    } else { // 在折线名称区域
      this.canvas.style.cursor = 'pointer'
    }
  }

  /**
   * 点击折线名称，显示/隐藏对应折线数据
   * @param  {Object} event 点击事件
   * @author Jiangzhiwei
   * @time   2021-11-06T19:41:56+0800
   */
  click (event) {
    const index = this.getPointInNameAreaIndex(event)
    if (index !== -1) { // 点击折线名称区域
      if (this.line.showSingle) { // 只显示一条
        // 隐藏的不是3条 或 选中的是隐藏的
        if (this.line.hideIndexes.length !== 3 || this.line.hideIndexes.some(idx => idx === index)) {
          this.line.hideIndexes = this.line.names.map((item, idx) => idx).filter(item => item !== index)
        }
      } else { // 可显示多条
        const idx = this.line.hideIndexes.findIndex(item => item === index)
        if (idx !== -1) { // 找到，表示该折线已隐藏
          this.line.hideIndexes.splice(idx, 1) // 移除
        } else { // 未找到，表示该折线显示中
          this.line.hideIndexes.push(index) // 加入
        }
      }
    }
  }

  /**
   * 绘制辅助线，以及折线信息面板
   * @author Jiangzhiwei
   * @time   2021-11-06T00:12:18+0800
   */
  drawAssistLine () {
    if (typeof this.assistLineX === 'number') {
      const info = this.getAreaInfo(this.assistLineX)
      if (info) { // 没有值表示辅助线已从左边移动出坐标轴了
        const x = this.getPosX(info.key)
        // console.log(info)
        this.cacheCtx.save()
        this.cacheCtx.beginPath()
        this.cacheCtx.moveTo(x, this.top)
        this.cacheCtx.lineTo(x, this.bottom)
        this.cacheCtx.strokeStyle = this.assist.color
        if (this.assist.dash && this.assist.dash.length) {
          const dash = this.assist.dash.map(item => item * this.ratio)
          this.cacheCtx.setLineDash(dash)
        }
        this.cacheCtx.stroke()
        this.cacheCtx.closePath()
        this.cacheCtx.restore()
        if (this.areaData[info.key]) { // 表示在坐标轴上
          this.drawInfoPannel({ x, tx: info.key }) // 绘制面板
          this.drawAssistPoint(info.key) // 绘制辅助线与折线的交叉点
          // console.log(this.areaData[info.key])
        }
      }
    }
  }

  /**
   * 绘制折线信息面板
   * @param  {Number} options.x 左上角x
   * @param  {Long} options.tx Math.floor(Date / 1000)，秒数
   * @author Jiangzhiwei
   * @time   2021-11-06T00:12:46+0800
   */
  drawInfoPannel ({ x, tx }) {
    const borderRadius = this.pannel.borderRadius * this.ratio // 圆角大小
    const padding = this.pannel.padding * this.ratio // 面板内间距
    const margin = this.pannel.margin * this.ratio // 面板与辅助线的间距
    const letterSpacingVer = this.pannel.letterSpacingVer * this.ratio // 行间距
    const figureMargin = this.pannel.figureMargin * this.ratio // 圆圈与右侧字体间距
    const fontSize = this.pannel.fontSize * this.ratio // 字体大小
    const dataInfo = this.areaData[tx] // 数据信息
    this.cacheCtx.font = `${fontSize}px ${this.fontFamily}`
    // 计算面板宽高
    const timeStr = this.formatDateTime(new Date(tx * 1000))
    const nameArr = [] // 记录每行的折线名称与对应的值
    const timeInfo = this.getTextInfo(timeStr)
    let height = padding * 2 + timeInfo.height
    let maxWidth = timeInfo.width
    const yArr = [] // 记录每行字对应高度位置
    let tempY = padding + timeInfo.height / 2 // 面板间距+半字高度
    yArr.push(tempY)
    tempY += timeInfo.height / 2 // 加上半字高度
    let unit = ''
    this.line.names.forEach((name, index) => {
      const axisAlign = this.line.axis[index]
      if (this.pannel.showUnit !== false) { // 如果没有手动设置不显示单位，则会显示单位
        if (axisAlign === 'left' || axisAlign === 'right') { // 如果数据属于左轴或者右轴，则取坐标轴单位
          unit = this[`${axisAlign}Axis`].unit
        } else { // 如果是额外数据，则取自定义的单位
          unit = this.line.extraUnits[index - this.line.axis.length] || ''
        }
      }
      const realText = `${name}${this.pannel.separator}${typeof dataInfo.values[index] !== 'undefined'
        ? dataInfo.values[index] + unit : this.pannel.noDataText}` // 名称+分隔符+数据+单位
      const nameInfo = this.getTextInfo(realText)
      nameArr.push({ text: realText, info: nameInfo })
      if (this.line.hideIndexes.every(item => item !== index)) { // 都不等，表示没有隐藏
        const singleLineWidth = nameInfo.height + figureMargin + nameInfo.width // 单行宽度：圆圈+间距+文字
        if (maxWidth < singleLineWidth) { // 更新最大宽度
          maxWidth = singleLineWidth
        }
        height += letterSpacingVer + nameInfo.height // 累加高度
        tempY += letterSpacingVer + nameInfo.height / 2 // 行间距+半字高度
        yArr.push(tempY)
        tempY += nameInfo.height / 2 // 加上半字高度
      } else {
        yArr.push(tempY)
      }
    })
    const width = padding * 2 + maxWidth
    // 计算x坐标位置
    if (this.lastPannelAlign === 'left') { // 上一帧显示的面板在竖直辅助线左侧
      if (x - width - margin < this.left) { // 如果当前在左侧，面板无法显示完全，则在右侧显示
        x += margin
        this.lastPannelAlign = 'right'
      } else { // 反之，则在左侧显示
        x -= width + margin
        this.lastPannelAlign = 'left'
      }
    } else { // 上一帧没显示或上一帧在竖直辅助线右侧显示
      if (x + width + margin > this.right) { // 如果当前在右侧，面板无法显示完全，则在左侧显示
        x -= width + margin
        this.lastPannelAlign = 'left'
      } else { // 反之在右侧显示
        x += margin
        this.lastPannelAlign = 'right'
      }
    }
    // 计算y坐标位置
    const y = (this.bottom + this.top - height) / 2
    // 画面板
    this.drawRoundRectPath({ x, y, r: borderRadius, width, height })
    this.cacheCtx.fillStyle = this.pannel.bgColor
    this.cacheCtx.fill()
    const left = x + padding
    this.drawText({
      x: left,
      y: y + yArr[0],
      text: timeStr,
      color: this.pannel.color,
      fontSize,
      verticalAlign: 'middle'
    }) // 绘制时间
    // 绘制信息
    nameArr.forEach((item, index) => {
      if (this.line.hideIndexes.every(item => item !== index)) { // 都不等，表示没有隐藏
        let offsetX = 0
        if (index < this.line.axis.length) { // 有对应坐标轴
          // 绘制圆圈
          offsetX += this.drawCircle({
            x: left,
            y: y + yArr[index + 1] + item.info.actualBoundingBoxDescent,
            r: item.info.height / 2,
            color: this.line.colors[index],
            borderColor: this.pannel.figureBorderColor,
            align: 'left'
          }) + figureMargin
        }
        // 绘制折线信息
        this.drawText({
          x: left + offsetX,
          y: y + yArr[index + 1],
          text: item.text,
          color: this.pannel.color,
          fontSize,
          verticalAlign: 'middle'
        })
      }
    })
  }

  /**
   * 绘制圆角矩形的路径
   * @param  {Number} options.x 左上角
   * @param  {Number} options.y 左上角
   * @param  {Number} options.r 半径
   * @param  {Number} options.width 宽度
   * @param  {Number} options.height 高度
   * @author Jiangzhiwei
   * @time   2021-11-06T00:15:32+0800
   */
  drawRoundRectPath ({ x, y, r, width, height }) {
    this.cacheCtx.beginPath()
    this.cacheCtx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2) // 左上角圆弧
    this.cacheCtx.lineTo(x + width - r, y)
    this.cacheCtx.arc(x + width - r, y + r, r, Math.PI * 3 / 2, 0) // 右上角圆弧
    this.cacheCtx.lineTo(x + width, y + height - r)
    this.cacheCtx.arc(x + width - r, y + height - r, r, 0, Math.PI / 2) // 右下角圆弧
    this.cacheCtx.lineTo(x + r, y + height)
    this.cacheCtx.arc(x + r, y + height - r, r, Math.PI / 2, Math.PI) // 左下角圆弧
    this.cacheCtx.closePath()
  }

  /**
   * 绘制辅助点
   * @param  {Long} tx Math.floor(Date / 1000)，秒数
   * @author Jiangzhiwei
   * @time   2021-11-06T18:36:42+0800
   */
  drawAssistPoint (tx) {
    const dataInfo = this.areaData[tx] // 数据信息
    const x = this.getPosX(tx)
    this.line.data.forEach((item, index) => {
      if (typeof dataInfo.values[index] !== 'undefined') {
        const y = this.getPosY(dataInfo.values[index], this.line.axis[index])
        this.drawCircle({
          x,
          y,
          r: this.assist.circleRadius * this.ratio,
          color: this.line.colors[index],
          borderColor: this.assist.showPointBorder === false ? null : this.assist.borderColor,
          align: 'center'
        })
      }
    })
  }

  draw () {
    this.drawTitles()
    // console.time()
    this.resetAxis()
    // console.timeEnd()
    this.drawLines()
  }

  /**
   * 绘制图像
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T10:56:28+0800
   */
  paint () {
    if (this.canvas.width > 0 && this.canvas.height > 0) {
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
      this.cacheCtx.clearRect(0, 0, this.cacheCanvas.width, this.cacheCanvas.height)
      this.draw()
      this.ctx.drawImage(this.cacheCanvas, 0, 0, this.canvas.width, this.canvas.height)
    }
  }

  /**
   * 循环渲染
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T10:55:45+0800
   */
  run () {
    const t = new Date().getTime()
    // console.time()
    this.paint()
    // console.timeEnd()
    this.clearSurplusData()
    if (this.isActive) {
      requestAnimationFrame(() => {
        // 移动下方坐标轴相关
        if (this.isMoving) {
          const dt = (new Date().getTime() - t) / 1000
          this.bottomAxisMin += dt
          this.bottomAxisMax += dt
          if (this.assistLineX) {
            this.assistLineX -= dt * (this.right - this.left) /
              (this.bottomAxis.size * this.bottomAxis.num)
          }
        }
        this.run()
      })
    }
    if (this.printRenderCostTime) { // 如果要打印渲染耗时
      this.totalRenderTime += new Date().getTime() - t
      this.renderTimeCount++
      if (this.renderTimeCount % this.renderTimes === 0) {
        window.console.log('渲染耗时：', this.totalRenderTime / this.renderTimes, '毫秒')
        this.totalRenderTime = 0
      }
    }
  }

  /**
   * 开始移动
   * @author Jiangzhiwei
   * @time   2021-11-06T22:28:09+0800
   */
  startMove () {
    this.isMoving = true
  }

  /**
   * 停止移动
   * @author Jiangzhiwei
   * @time   2021-11-06T22:28:43+0800
   */
  stopMove () {
    this.isMoving = false
  }

  /**
   * 定时监听div大小是否发生变化
   * @Author   Jiangzhiwei
   * @datetime 2021-11-08T13:40:54+0800
   */
  watchResize () {
    this.timeout = setTimeout(() => {
      this.resize()
      this.watchResize()
    }, this.watchInterval)
  }

  /**
   * div大小改变则调整canvas大小相关属性
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T10:56:47+0800
   */
  resize () {
    const dom = this.canvas.parentNode
    const width = this.getWidth(dom)
    const height = this.getHeight(dom)
    if (typeof this.parentWidth === 'undefined' ||
      this.parentWidth !== width || this.parentHeight !== height) {
      this.parentWidth = width
      this.parentHeight = height
      // console.log(width, height)
      this.canvas.width = width * this.ratio
      this.canvas.height = height * this.ratio
      this.cacheCanvas.width = width * this.ratio
      this.cacheCanvas.height = height * this.ratio
      this.canvas.style.width = width + 'px'
      this.canvas.style.height = height + 'px'
    }
  }

  getWidth (dom) {
    return Number.parseInt(getComputedStyle(dom).width)
  }

  getHeight (dom) {
    return Number.parseInt(getComputedStyle(dom).height)
  }

  /**
   * 重置左右坐标轴
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T10:58:49+0800
   */
  resetAxis () {
    // 计算左右坐标对应折线的最大最小值
    let leftMin, leftMax, xMin
    let rightMin, rightMax
    // 约定所有时间坐标轴数据递增
    // 先确定最小值
    const showData = this.line.data.filter((item, index) => this.line.hideIndexes.every(itemIndex => itemIndex !== index))
    showData.forEach((item, index) => {
      if (this.line.axis[index] === 'left') { // y轴对齐左坐标轴
        const minObject = this.getMin(item, leftMin, xMin)
        leftMin = minObject.y
        xMin = minObject.x
      } else if (this.line.axis[index] === 'right') { // y轴对齐右坐标轴
        const minObject = this.getMin(item, rightMin, xMin)
        rightMin = minObject.y
        xMin = minObject.x
      }
    })
    // 然后确定底部坐标轴
    this.calcBottomAxis(xMin)
    // 然后确定最大值
    showData.forEach((item, index) => {
      if (this.line.axis[index] === 'left') { // y轴对齐左坐标轴
        leftMax = this.getMax(item, leftMin, leftMax)
      } else { // y轴对齐右坐标轴
        rightMax = this.getMax(item, rightMin, rightMax)
      }
    })
    // console.log(rightMin, rightMax)
    this.leftMin = leftMin
    this.leftMax = leftMax
    this.rightMin = rightMin
    this.rightMax = rightMax
    // console.time()
    this.resetLeftAxis()
    this.resetRightAxis()
    this.resetBottomAxis()
    // console.timeEnd()
    // 用于坐标轴高度变换时改变折线高度
    this.preTop = this.top
    this.preBottom = this.bottom
  }

  /**
   * 计算纵坐标最小值的公共方法，并将横坐标最小值计算出来
   * @param  {Array} arr 一条折线的数据数组
   * @param  {Number} yMin 折线需要显示部分纵坐标最小值
   * @param  {Number} xMin 折线需要显示部分横坐标最小值
   * @return {Object} 横坐标最小值与纵坐标最小值
   * @author Jiangzhiwei
   * @time   2021-11-06T11:46:05+0800
   */
  getMin (arr, yMin, xMin) {
    if (this.bottomAxisMin) { // 表示已经确定过时间轴了，则找一个时间轴上新的最小值
      let tempX
      for (let i = 0; i < arr.length; i++) {
        const point = arr[i]
        const t = point.x
        if (t > this.bottomAxisMax + this.maxUpdateTime) { // 值超过最大值，则不再判断
          break
        } else if (t >= this.bottomAxisMin - this.maxUpdateTime) { // 值未小于时间轴最小值
          if (typeof yMin === 'undefined' || yMin > point.y) {
            yMin = point.y
          }
          if (typeof tempX === 'undefined') { // 仅第一次时会是undefined，即取第一个值，因为根据约定，第一个值最小
            tempX = point.x
            if (typeof xMin === 'undefined' || xMin > tempX) {
              xMin = tempX
            }
          }
        }
      }
    } else { // 表示之前没有确定过时间轴，则从每组第一个数据中确定时间轴最小值，此时暂时不计算最小值
      if (typeof xMin === 'undefined' || xMin < arr[0].x) {
        xMin = arr[0].x
      }
    }
    // console.log(min)
    return { x: xMin, y: yMin }
  }

  /**
   * 计算纵坐标最大值的公共方法，需要根据最小值计算最大值
   * @param  {Array} arr 一条折线的数据数组
   * @param  {Number} min 折线需要显示部分纵坐标最小值
   * @param  {Number} max 折线需要显示部分纵坐标最大值
   * @return {Number} 折线需要显示部分数据的最大值
   * @author Jiangzhiwei
   * @time   2021-11-06T13:01:09+0800
   */
  getMax (arr, min, max) {
    if (typeof min !== 'undefined') { // 找到了最小值，再找最大值
      for (let i = arr.length - 1; i >= 0; i--) {
        const point = arr[i]
        if (point.x < this.bottomAxisMin - this.maxUpdateTime) { // 值小于最小值，则不再判断
          break
        } else if (point.x <= this.bottomAxisMax + this.maxUpdateTime) { // 值未超过时间轴最大值
          if (typeof max === 'undefined' || max < point.y) {
            max = point.y
          }
        }
      }
    }
    return max
  }

  /**
   * 重置左坐标轴
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T10:59:09+0800
   */
  resetLeftAxis () {
    if (typeof this.leftMin !== 'undefined') {
      this.calcLeftAxis()
      // console.time()
      this.drawLeftAxis()
      // console.timeEnd()
    }
  }

  /**
   * 重置右坐标轴
   * @Author   Jiangzhiwei
   * @datetime 2021-11-04T10:59:45+0800
   */
  resetRightAxis () {
    if (typeof this.rightMin !== 'undefined') {
      this.calcRightAxis()
      this.drawRightAxis()
    }
  }

  /**
   * 重置下方坐标轴
   * @author Jiangzhiwei
   * @time   2021-11-04T23:52:22+0800
   */
  resetBottomAxis () {
    this.drawBottomAxis()
  }

  /**
   * 计算左方坐标相关数据
   * @author Jiangzhiwei
   * @time   2021-11-04T23:52:52+0800
   */
  calcLeftAxis () {
    this.calcVerticalAxis('left')
  }

  /**
   * 计算右方坐标相关数据
   * @author Jiangzhiwei
   * @time   2021-11-05T00:12:32+0800
   */
  calcRightAxis () {
    this.calcVerticalAxis('right')
  }

  /**
   * 计算竖直坐标公共方法
   * @param  {String} axis 对齐，left、right
   * @author Jiangzhiwei
   * @time   2021-11-07T16:48:33+0800
   */
  calcVerticalAxis (axis) {
    // 如果坐标已经创建过，则判断1.最大值最小值是否越界2.最大值最小值差值小于高低坐标差值值的一半
    if (this[`${axis}AxisMin`] &&
      this.top === this.preTop && this.bottom === this.preBottom) { // 坐标已经创建过 且 未发生变化
      if (this[`${axis}Min`] > this[`${axis}AxisMin`] &&
        this[`${axis}Max`] < this[`${axis}AxisMax`]) { // 数据未超过当前坐标轴最大最小值
        if (this[`${axis}Max`] - this[`${axis}Min`] >
          (this[`${axis}AxisMax`] - this[`${axis}AxisMin`]) / 4) { // 数据高度超过四分之一
          return
        }
      }
    }
    // 重新计算高低坐标
    const axisInfo = this.getVerticalAxisInfo(this[`${axis}Min`], this[`${axis}Max`],
      this[`${axis}Axis`].num, this[`${axis}Axis`].min, this[`${axis}Axis`].max)
    this[`${axis}AxisMin`] = axisInfo.min
    this[`${axis}AxisMax`] = axisInfo.max
    this[`${axis}AxisGap`] = axisInfo.size
    this[`${axis}AxisStep`] = axisInfo.step
    this[`${axis}AxisValues`] = axisInfo.values
  }

  /**
   * 计算底部坐标相关数据
   * @author Jiangzhiwei
   * @time   2021-11-05T00:12:58+0800
   */
  calcBottomAxis (minTime) {
    if (!this.bottomAxisMin) { // 如果没有计算或设置坐标轴的最小时间值，则计算
      if (this.bottomAxis.showNowTime) { // 是否显示当前时刻数据
        this.bottomAxisMax =
          Math.floor(new Date().getTime() / 1000) - this.bottomAxis.delay // 当前时间前几秒
        this.bottomAxisMin = this.bottomAxisMax - this.bottomAxis.num * this.bottomAxis.size
      } else { // 不是，则根据数据显示
        const gap = this.bottomAxis.num * this.bottomAxis.size // 最大坐标与最小坐标间隔的时长
        if (this.bottomAxis.dataAlign === 'right') { // 数据最小时间对齐(右侧坐标 + 延迟)
          this.bottomAxisMax = minTime - this.bottomAxis.delay
          this.bottomAxisMin = this.bottomAxisMax - gap
        } else { // 最小时间对齐左侧坐标
          this.bottomAxisMin = minTime - this.bottomAxis.delay
          this.bottomAxisMax = this.bottomAxisMin + gap
        }
      }
    }
    this.bottomAxisStep =
      (this.right - this.left) / (this.bottomAxis.num * this.bottomAxis.size)

    // 计算数据区域
    this.areaData = {}
    const total = this.bottomAxis.size * this.bottomAxis.num + 1 // 区域总数(没对齐时会多1个)
    const dt = this.bottomAxisMin - Math.floor(this.bottomAxisMin) // 小数部分
    for (let i = 0; i < total; i++) {
      const time = Math.ceil(this.bottomAxisMin + i)
      // const sx = this.formatDateTime(new Date(longTime))
      this.areaData[time] = {
        min: this.left + (0.5 - dt + i) * this.bottomAxisStep,
        max: this.left + (1.5 - dt + i) * this.bottomAxisStep,
        key: time,
        values: [] // 用于记录所有折线当前点的数据
      }
    }
  }

  parseInt (s) {
    return Number.parseInt(s, 10)
  }

  /**
   * 将日期字符串转化为秒数
   * @param  {String} timeStr 格式如yyyy-MM-dd HH:mm:ss
   * @return {Long} 秒数
   * @author Jiangzhiwei
   * @time   2021-11-06T13:13:33+0800
   */
  getTimeSeconds (timeStr) {
    const matches = timeStr.match(this.regex)
    const date = new Date(this.parseInt(matches[1]),
      this.parseInt(matches[2]) - 1, this.parseInt(matches[3]))
    date.setHours(this.parseInt(matches[4]), this.parseInt(matches[5]),
      this.parseInt(matches[6]))
    return date.getTime() / 1000 // 秒
  }

  /**
   * 将日期格式化为时间，如HH:mm:ss
   * @param  {Object} date Date
   * @return {String} HH:mm:ss
   * @author Jiangzhiwei
   * @time   2021-11-06T13:16:13+0800
   */
  formatTime (date) {
    let h = date.getHours()
    if (h < 10) {
      h = '0' + h
    }
    let m = date.getMinutes()
    if (m < 10) {
      m = '0' + m
    }
    let s = date.getSeconds()
    if (s < 10) {
      s = '0' + s
    }
    return `${h}:${m}:${s}`
  }

  /**
   * 将日期格式化为日期，如yyyy-MM-dd HH:mm:ss
   * @param  {Object} date Date
   * @return {String} yyyy-MM-dd HH:mm:ss
   * @author Jiangzhiwei
   * @time   2021-11-06T13:16:59+0800
   */
  formatDateTime (date) {
    const y = date.getFullYear()
    let M = date.getMonth() + 1
    if (M < 10) {
      M = '0' + M
    }
    let d = date.getDate()
    if (d < 10) {
      d = '0' + d
    }
    let h = date.getHours()
    if (h < 10) {
      h = '0' + h
    }
    let m = date.getMinutes()
    if (m < 10) {
      m = '0' + m
    }
    let s = date.getSeconds()
    if (s < 10) {
      s = '0' + s
    }
    return `${y}-${M}-${d} ${h}:${m}:${s}`
  }

  /**
   * 根据折线最小最大值，获取坐标相应信息
   * @param  {Number} minValue 折线最小值
   * @param  {Number} maxValue 折线最大值
   * @param  {Integer} num 刻度格数
   * @param  {Number} fixedMinValue 固定最小值
   * @param  {Number} fixedMaxValue 固定最大值
   * @return {Object} 坐标轴相应信息
   * @author Jiangzhiwei
   * @time   2021-11-06T13:26:51+0800
   */
  getVerticalAxisInfo (minValue, maxValue, num, fixedMinValue, fixedMaxValue) {
    const ratio = 1 / num // 上下各扩展1/num
    // const num = 5 // 几个刻度
    let diff = maxValue - minValue // 折线最大值与最小值相差
    let size // 每格刻度的数值差，如每格刻度代表1
    let min, max // 最大最小坐标值
    const values = [] // 每个刻度线的值
    if (diff === 0) { // 折线没有变化
      if (typeof fixedMinValue !== 'undefined' && typeof fixedMaxValue === 'undefined') { // 仅固定了最小值
        min = fixedMinValue
        // 重新设置最小值后，相当于有变化了，重新计算差值
        diff = maxValue - min // 差值
        max = maxValue + diff * ratio // 向上扩展
        diff = max - min
        size = diff / num
        if (size > 1) {
          size = this.beautifySize(size)
        } else {
          size = 1
        }
        // 记录所有刻度值
        for (let i = 0; i <= num; i++) {
          values.push(min + i * size)
        }
        max = values[num]
      } else if (typeof fixedMinValue === 'undefined' && typeof fixedMaxValue !== 'undefined') { // 仅固定了最大值
        max = fixedMaxValue
        // 重新设置最大值后，相当于有变化了，重新计算差值
        diff = max - minValue // 差值
        min = minValue - diff * ratio // 向下扩展
        diff = max - min
        size = diff / num
        if (size > 1) {
          size = this.beautifySize(size)
        } else {
          size = 1
        }
        // 记录所有刻度值
        for (let i = num; i >= 0; i--) {
          values.push(max - i * size)
        }
        min = values[0]
      } else if (typeof fixedMinValue !== 'undefined' && typeof fixedMaxValue !== 'undefined') { // 固定了最大最小值
        min = fixedMinValue
        max = fixedMaxValue
        size = (max - min) / num
        // 记录所有刻度值
        for (let i = 0; i < num; i++) {
          values.push(min + i * size)
        }
        values[num] = max // 以防小数丢失精度，最大值固定赋值
      } else { // 没有固定最大最小值
        size = 1 // 默认刻度间隔为1
        min = Math.floor(minValue - num / 2)
        // 记录所有刻度值
        for (let i = 0; i <= num; i++) {
          values.push(min + i * size)
        }
        max = values[num]
      }
    } else { // 折线有变化
      if (typeof fixedMaxValue !== 'undefined') { // 固定了最大值
        max = fixedMaxValue
      } else {
        max = maxValue
      }
      if (typeof fixedMinValue !== 'undefined') { // 固定了最小值
        min = fixedMinValue
      } else {
        min = minValue
      }
      if (typeof fixedMaxValue === 'undefined') { // 没有固定最大值
        max = maxValue + (max - min) * ratio // 坐标轴的最大值
      }
      if (typeof fixedMinValue === 'undefined') { // 没有固定最小值
        min = minValue - (max - min) * ratio // 坐标轴的最小值
        // minValue = this.beautifyMin(minValue)
      }
      diff = max - min // 高低坐标轴差值
      size = diff / num
      if (size > 1) { // 如果值大于1，则所有刻度取整
        if (typeof fixedMinValue !== 'undefined' && typeof fixedMaxValue === 'undefined') { // 仅固定最小值
          size = this.beautifySize(size)
          // 记录所有刻度值
          for (let i = 0; i <= num; i++) {
            values.push(min + i * size)
          }
          max = values[num]
        } else if (typeof fixedMinValue === 'undefined' && typeof fixedMaxValue !== 'undefined') { // 仅固定最大值
          size = this.beautifySize(size)
          // 记录所有刻度值
          for (let i = num; i >= 0; i--) {
            values.push(max - i * size)
          }
          min = values[0]
        } else if (typeof fixedMinValue !== 'undefined' && typeof fixedMaxValue !== 'undefined') { // 固定最大最小值
          // 记录所有刻度值
          for (let i = 0; i <= num; i++) {
            values.push(Math.floor(min + i * size))
          }
          values[0] = min
          values[num] = max
        } else { // 没有固定最大最小值
          size = this.beautifySize(size)
          // size = Math.ceil(size) // 每格刻度取整
          // minValue = Math.floor(minValue) // 坐标轴最小值向下取整
          min = this.beautifyMin(min, minValue, size)
          // 记录所有刻度值
          for (let i = 0; i <= num; i++) {
            values.push(min + i * size)
          }
          max = values[num]
        }
      } else { // 如果小于1大于0（等于0情况之前已经过滤了），则
        let mul = 10 // 需要乘以多少能大于0
        while (size * mul < 1) {
          mul *= 10
        }
        // 先操作整数
        size = Math.ceil(size * mul)
        min = Math.floor(min * mul)
        // 记录所有刻度值
        for (let i = 0; i <= num; i++) {
          values.push((min + i * size) / mul) // 处理成小数
        }
        // 处理成小数
        size /= mul
        min /= mul
      }
    }
    const step = (this.bottom - this.top) / (size * num) // 像素/距离
    return { min, size, step, max, values }
  }

  /**
   * 第一个时间字符串是否小于第二个时间字符串
   * @param  {String} a yyyy-MM-dd HH:mm:ss
   * @param  {String} b yyyy-MM-dd HH:mm:ss
   * @return {Boolean} 前面值是否小于后面值
   * @author Jiangzhiwei
   * @time   2021-11-06T19:44:20+0800
   */
  isLessThan (a, b) {
    a = Number.parseInt(a.replace(/:|\s|-/g, ''), 10)
    b = Number.parseInt(b.replace(/:|\s|-/g, ''), 10)
    return a < b
  }

  /**
   * 根据折线x的数据，换算为canvas中的x坐标
   * @param  {Long} x Math.floor(Date / 1000)，秒数
   * @return {Number} x坐标
   * @author Jiangzhiwei
   * @time   2021-11-06T13:23:37+0800
   */
  getPosX (x) {
    const dx = x - this.bottomAxisMin
    return this.left + dx * this.bottomAxisStep
  }

  /**
   * 根据折线y的数据，换算为canvas中的y坐标
   * @param  {Number} y 折线的y数据
   * @param  {String} axis left 或者 right
   * @return {Number} y坐标
   * @author Jiangzhiwei
   * @time   2021-11-06T13:24:39+0800
   */
  getPosY (y, axis) {
    return this.bottom - (y - this[`${axis}AxisMin`]) * this[`${axis}AxisStep`]
  }

  /**
   * 事件发生时的点位置，是否在坐标区域中
   * @param  {Object} event 事件
   * @return {Boolean} 是否在坐标区域中
   * @author Jiangzhiwei
   * @time   2021-11-06T13:22:45+0800
   */
  isPointInArea (event) {
    const point = this.getPointInCanvas(event)
    this.ctx.beginPath()
    this.ctx.rect(this.left + 1, this.top + 1, this.right - this.left - 2,
      this.bottom - this.top - 2)
    this.ctx.closePath()
    return this.ctx.isPointInPath(point.x, point.y)
  }

  /**
   * 获取点所在名字区域的序号，-1表示不在
   * @param  {Object} event 事件
   * @return {Integer} 区域序号
   * @author Jiangzhiwei
   * @time   2021-11-06T17:30:02+0800
   */
  getPointInNameAreaIndex (event) {
    let index = -1 // 所属折线名字的矩形路径的序号
    const point = this.getPointInCanvas(event)
    this.lineNameRectInfos.some((item, idx) => {
      this.ctx.beginPath()
      this.ctx.rect(item.x, item.y, item.width, item.height)
      this.ctx.closePath()
      if (this.ctx.isPointInPath(point.x, point.y)) {
        index = idx
        return true
      }
    })
    return index
  }

  /**
   * 将浏览器上的点位置转化为canvas中的点位置
   * @param  {Object} event 事件
   * @return {Object} 位置信息
   * @author Jiangzhiwei
   * @time   2021-11-06T13:21:42+0800
   */
  getPointInCanvas (event) {
    const bound = this.canvas.getBoundingClientRect()
    const x = (event.clientX - bound.left) * this.ratio
    const y = (event.clientY - bound.top) * this.ratio
    return { x, y }
  }

  /**
   * 根据canvas上的x位置，来获取对应坐标轴上折线的信息
   * @param  {Number} x canvas的x坐标
   * @return {Object} 包括了折线信息
   * @author Jiangzhiwei
   * @time   2021-11-06T13:19:14+0800
   */
  getAreaInfo (x) {
    let info
    Object.keys(this.areaData).some(key => {
      const map = this.areaData[key]
      if (x >= map.min && x < map.max) {
        info = map
        return true
      }
    })
    return info
  }

  /**
   * 根据文字获取宽高
   * @param  {String} text 文本
   * @return {Object} 宽高信息
   * @author Jiangzhiwei
   * @time   2021-11-06T13:20:58+0800
   */
  getTextInfo (text) {
    this.cacheCtx.textBaseline = 'bottom'
    const mt = this.cacheCtx.measureText(text)
    if (!mt.actualBoundingBoxAscent || !mt.actualBoundingBoxDescent) {
      if (!this.measureTextError) {
        this.measureTextError = '该浏览器不支持actualBoundingBoxAscent等属性，将会导致实时图不能正常显示'
        this.printErrorMsg(this.measureTextError)
      }
    }
    mt.height = mt.actualBoundingBoxAscent + mt.actualBoundingBoxDescent
    return mt
  }

  /**
   * 添加数据
   * @param    {Array} data [{ k1, k2, k3 }]
   * @param    {Boolean} excludeSameTimeData 是否排除相同时间点的数据。如果排除，则同一条线上相同时间点数据无法再次添加
   * @Author   Jiangzhiwei
   * @datetime 2021-11-08T10:04:21+0800
   */
  addData (data, excludeSameTimeData = true) {
    const hasMap = {} // 记录添加了哪些折线的数据
    const timeKey = this.line.keys[this.line.keys.length - 1]
    // 循环加入数据
    data.forEach(item => {
      this.line.names.forEach((name, index) => {
        const key = this.line.keys[index] // 折线对应数据里的key
        if (key && typeof item[key] !== 'undefined') { // 如果key设置了 且 有数据，则准备插入数据
          let lineData = this.line.data[index] // 一条线的数据集合
          if (!lineData) { // 如果此折线当前没有数据，则初始设置
            lineData = []
            this.line.data[index] = lineData
          }
          const time = item[timeKey]
          let ableAdd = true // 是否能够添加
          if (excludeSameTimeData) { // 如果需要排除相同时间点数据
            if (lineData.some(item => item.x === time)) { // 表示已有该时间点的数据
              ableAdd = false
              // console.log(ableAdd)
            }
          }
          if (ableAdd) { // 能够添加
            const value = item[key]
            lineData.push({ x: time, y: value })
            if (!hasMap[index]) { // 如果没有标记，则进行标记
              hasMap[index] = true
            }
          }
        }
      })
    })
    // 对于变化了的折线数据，进行排序
    Object.keys(hasMap).forEach(key => {
      this.line.data[key].sort((a, b) => {
        return a.x <= b.x ? -1 : 1
      })
    })
  }

  /**
   * 获取坐标轴数据
   * @return   {Array} [...]
   * @Author   Jiangzhiwei
   * @datetime 2022-01-10T16:32:13+0800
   */
  getData () {
    return this.line.data
  }

  /**
   * 重新加载坐标轴数据
   * @param    {Array} data [...]
   * @Author   Jiangzhiwei
   * @datetime 2022-01-10T16:32:49+0800
   */
  reloadData (data) {
    this.line.data = data
  }

  /**
   * 移除多余的数据
   * @Author   Jiangzhiwei
   * @datetime 2021-11-08T14:10:26+0800
   */
  clearSurplusData () {
    this.surplusRecords.forEach((num, index) => {
      if (num > this.clearSurplusNum) { // 数据过多，进行移除
        this.line.data[index] = this.line.data[index].slice(num + 1)
      }
    })
  }

  /**
   * 美化刻度大小，使数字看起来更整齐
   * @param    {Number} size 美化前刻度间隔大小
   * @return   {Number} 美化后刻度大小
   * @Author   Jiangzhiwei
   * @datetime 2022-02-22T14:13:27+0800
   */
  beautifySize (size) {
    if (size >= 1 && size <= 10) {
      return Math.ceil(size)
    } else if (size > 10 && size <= 100) {
      return Math.ceil(size / 5) * 5
    } else if (size > 100) {
      const num = Math.floor(Math.log10(size))
      const n = Math.pow(10, num - 1)
      return Math.ceil(size / n) * n
    }
  }

  /**
   * 美化最小刻度，使数字看起来更整齐
   * @param    {Number} min 美化前最小刻度
   * @param    {Number} minValue 数据最小值
   * @param    {Number} size 刻度大小
   * @return   {Number} 美化后最小刻度
   * @Author   Jiangzhiwei
   * @datetime 2022-02-22T14:14:50+0800
   */
  beautifyMin (min, minValue, size) {
    if (min === 0) {
      return min
    }
    min = Math.floor(min / size) * size
    if (minValue - min < size / 2) { // 最小值距离最小刻度过近
      min -= size / 2
    } else {
      min += size / 2
    }
    return min
  }

  /**
   * 格式化数字，使数字大小更容易分辨
   * @param    {Number} value 原数字
   * @param    {Number} num 分隔位数
   * @param    {String} c 分隔字符
   * @return   {String} 格式化后的数字
   * @Author   Jiangzhiwei
   * @datetime 2022-02-22T14:35:05+0800
   */
  formatValue (value, num = 3, c = ',') {
    let temp
    let flag
    if (value < 0) {
      temp = -value + ''
      flag = '-'
    } else {
      temp = value + ''
      flag = ''
    }
    const temps = temp.split('.')
    let tempFront = temps[0]
    let result = temps[1] ? '.' + temps[1] : ''
    while (tempFront.length > num) { // 数还足够大
      result = c + tempFront.substr(-num) + result
      tempFront = tempFront.substring(0, tempFront.length - num)
    }
    result = flag + tempFront + result
    return result
  }

  /**
   * 开始移动下方坐标轴，暂未使用，目前通过动画帧调用方式
   * @author Jiangzhiwei
   * @time   2021-11-05T00:18:28+0800
   */
  startAxis () {
    if (!this.axisStarted) { // 没有动起来，则使他动起来
      this.axisStarted = true
      setTimeout(() => {
        this.moveAxis()
      }, this.bottomAxis.delay * 1000)
    }
  }

  /**
   * 移动下方坐标轴，暂未使用
   * @author Jiangzhiwei
   * @time   2021-11-05T00:19:50+0800
   */
  moveAxis () {
    this.bottomAxisMin += this.bottomAxis.delay
    this.moveTimeout = setTimeout(() => {
      this.moveAxis()
    }, this.bottomAxis.delay * 1000)
  }

  /**
   * 清除处理，停止循环并移除窗口变化监听事件
   * @author Jiangzhiwei
   * @time   2021-11-07T16:46:08+0800
   */
  dispose () {
    this.isActive = false
    clearTimeout(this.timeout)
    // window.removeEventListener('resize', this.resizeEvent)
  }

  /**
   * 打印错误信息
   * @param    {String} msg 错误信息
   * @Author   Jiangzhiwei
   * @datetime 2022-04-01T11:33:53+0800
   */
  printErrorMsg (msg) {
    window.console.error(`<yc-realtimeline>：${msg}`)
  }
}

function init (dom, option) {
  return new Graph(dom, option)
}

export default {
  init
}
